grammar PascalTo3addr;

@header {
package output;

import java.util.Map;
import java.util.HashMap;
import runtime.*;
}

@members {

	List<Map<String, Type>> types = new ArrayList<Map<String, Type>>();
	List<Map<String, Integer>> ids = new ArrayList<Map<String, Integer>>();
	List<Integer> idCount = new ArrayList<Integer>();	
	
	protected void pushScope() {
		types.add(new HashMap<String, Type>());
		ids.add(new HashMap<String, Integer>());
		idCount.add(0);
	}
	protected void popScope() {
		types.remove(types.size() - 1);
		ids.remove(ids.size() - 1);
		idCount.remove(idCount.size() - 1);
	}
	protected int allocateTempVariable(Type type) {
		int rv = idCount.get(idCount.size() - 1);
		idCount.set(idCount.size() - 1, rv + 1);
		code.add(type + " V" + rv); 
		return rv;
	}
	protected void declare(String name, Type type) {
		Map<String, Type> last = types.get(types.size() - 1);
		if (last.containsKey(name)) {
			throw new CompilationError("Variable '" + name + "' is already declared in this scope");
		}
		last.put(name, type);
		int id = idCount.get(idCount.size() - 1) + 1;
		idCount.set(idCount.size() - 1, id);
		ids.get(ids.size() - 1).put(name, id);
		code.add(type.platformName() + " V" + id);
	}
	protected Type resolveType(String name) {
		int curr = types.size() - 1;
		while (curr >= 0) {
			if (types.get(curr).containsKey(name)) {
				return types.get(curr).get(name);
			}
		}
		throw new CompilationError("Name '" + name + "' is not found");
	}
	protected int resolveID(String name) {
		int curr = types.size() - 1;
		while (curr >= 0) {
			if (ids.get(curr).containsKey(name)) {
				return ids.get(curr).get(name);
			}
		}
		throw new CompilationError("Name '" + name + "' is not found");
	}
	protected final List<String> code = new ArrayList<String>();
	
	protected int labelCount = 0;
	protected String newLabel() {
		String rv = "l" + labelCount++;
		code.add(rv + ":");
		return rv;
	}
}

parse returns [List<String> result] : { code.clear(); } module { $result = new ArrayList<String>(code); code.clear(); } ;

module : 'program' ID ';' block '.' ; 

number returns [String text, Type type]
	: INT   {$text = $INT.text;   $type = IntType.instance; } 
	| FLOAT {$text = $FLOAT.text; $type = RealType.instance; }
	;

idList returns [List<String> list]
	: { $list = new ArrayList<String>(); } a=ID { list.add($a.text); } (',' b=ID { list.add($b.text); } )* ;

block returns [String label] 
	: { 
		$label = newLabel();
		pushScope(); 
	  } declarations statementSequence { 
	  	popScope(); 
	  } 
	;

declarations : (variableDeclBlock)? subprogDeclList ;

variableDeclBlock : 'var' variableDecl ';' (variableDecl ';')* ;

variableDecl 
	: idList ':' type { 
		for (String id : $idList.list) {
			declare(id, $type.result);
		}
	  } ;

type returns [Type result] 
	: 'integer' { $result = IntType.instance; }
	| 'boolean' { $result = BooleanType.instance; }
	| 'real'    { $result = RealType.instance; }
	| 'char'    { $result = CharType.instance; }
	| arrayType { $result = $arrayType.result; } 
	;
  
arrayType returns [Type result]
	: 'array' {
		List<Integer> lowers = new ArrayList<Integer>();
		List<Integer> uppers = new ArrayList<Integer>();
		List<Type> types = new ArrayList<Type>();
	  } '[' f=subrange {
	  	lowers.add($f.lower);
	  	uppers.add($f.upper);
	  	types.add($f.type);
	  } (',' s=subrange {
	  	lowers.add($s.lower);
	  	uppers.add($s.upper);
	  	types.add($s.type);
	  } )* ']' 'of' type {
	  	Type current = $type.result;
	  	for (int i = 0; i < lowers.size(); ++i) {
	  		current = new ArrayType(current, lowers.get(i), uppers.get(i), types.get(i));
	  	}
	  	$result = current;
	  } ;

subrange returns [int lower, int upper, Type type] 
	: l=enumConstant '..' u=enumConstant {
		if (!$l.type.equals($u.type)) {
			throw new CompilationError("Types of left and right bounds are not equal: " 
			                           + $l.type + " for left and " + $u.type + " for right");
		}
		$lower = $l.value;
		$upper = $u.value;
		$type  = $l.type;
	  }
	;
	
enumConstant returns [int value, Type type]
	: INT 		{ $value = Integer.parseInt($INT.text); $type = IntType.instance;     }
	| CHAR		{ $value = $CHAR.text.charAt(1);        $type = CharType.instance;    } 
	| 'true'    { $value = 1;                           $type = BooleanType.instance; }
	| 'false'   { $value = 0;                           $type = BooleanType.instance; }
	;

constant returns [String text, Type type]
	: number  {$text = $number.text; $type = $number.type;         }
	| 'true'  {$text = "true";       $type = BooleanType.instance; } 
	| 'false' {$text = "false";      $type = BooleanType.instance; };

statementSequence : 'begin' statement (';' statement)* 'end' ;

statement 
		: ifStatement
		| whileStatement 
		| repeatStatement
		| forStatement
		| iOStatement 
		| statementSequence 
		| 
		| 'break'     { if (true) { throw new CompilationError("break is not implemented");    } }
		| 'continue'  { if (true) { throw new CompilationError("continue is not implemented"); } }
		| assignment
		| functionCall 
		;
		
repeatStatement	: 'repeat'  statement (';' statement)* 'until'  expression ;

assignment 
	: qualifiedName ':=' expression {
		if ($qualifiedName.type.equals($expression.type)) {
			code.add($qualifiedName.access + " = V" + $expression.variable);
		} else if ($qualifiedName.type.equals(RealType.instance) && $expression.type.equals(IntType.instance)) {
			int newVariable = allocateTempVariable(RealType.instance);
			code.add("V" + newVariable + " = (" + RealType.instance + ") V" + $expression.variable);
			code.add($qualifiedName.access + " = V" + newVariable);
		}
	  } 
	;
 
ifStatement : 'if' expression 'then' statement ('else' statement)? ;

whileStatement : 'while' expression 'do' statement ;

forStatement : 'for' ID ':=' forRange 'do' statement ;
forRange : expression whichWay expression;
whichWay : 'to' | 'downto' ;

iOStatement 
	: 'read' '(' qualifiedList ')' {
		List<String> access = $qualifiedList.access;
		List<Type> types = $qualifiedList.types;
		for (int i = 0; i < types.size(); ++i) {
			int variable = allocateTempVariable(types.get(i));
			code.add("call read" + types.get(i) + " &V" + variable);
			code.add(access.get(i) + " = V" + variable);
		}
	  } 
	| 'readln' ( '(' qualifiedList ')' {
		List<String> access = $qualifiedList.access;
		List<Type> types = $qualifiedList.types;
		for (int i = 0; i < types.size(); ++i) {
			int variable = allocateTempVariable(types.get(i));
			code.add("call read" + types.get(i) + " &V" + variable);
			code.add(access.get(i) + " = V" + variable);
		}
	  } )? {
	  	code.add("call readLine");
	  } 
	| 'write' '(' expList ')' {
		List<Integer> ids = $expList.ids;
		List<Type> types = $expList.types;
		for (int i = 0; i < ids.size(); ++i) {
			if (types.get(i) instanceof ArrayType) {
				throw new CompilationError("write(array) is not supported");
			} else {
				code.add("param V" + ids.get(i));
				code.add("call write" + types.get(i));
			}
		}
	  }
	| 'writeln' ( '(' expList ')' {
		List<Integer> ids = $expList.ids;
		List<Type> types = $expList.types;
		for (int i = 0; i < ids.size(); ++i) {
			if (types.get(i) instanceof ArrayType) {
				throw new CompilationError("write(array) is not supported");
			} else {
				code.add("param V" + ids.get(i));
				code.add("call write" + types.get(i));
			}
		}
	  })? {
	  	code.add("call writeLine");
	  } 
	;

actualParameters returns [List<Integer> ids, List<Type> types] 
	: '(' expList ')' { 
		$ids = $expList.ids; 
		$types = $expList.types; 
	  } 
	;

expList returns [List<Integer> ids, List<Type> types]
	: { 
		$ids = new ArrayList<Integer>(); 
		$types = new ArrayList<Type>();
	  } a=expression { 
	  	$ids.add($a.variable); 
	  	$types.add($a.type);
	  } ( ',' b=expression { 
	  	$ids.add($b.variable);
	  	$types.add($b.type); 
	  } )* ;

expression returns [int variable, Type type]
	: a=simpleExpression { 
		$variable = $a.variable;
		$type = $a.type;
	  } ( relation b=simpleExpression {
	  	if ($type.equals($b.type)) {
	  		int newVariable = allocateTempVariable(BooleanType.instance);
	  		code.add("V" + newVariable + " = V" + $variable + " " + $relation.text + " V" + $b.variable);
	  		$variable = newVariable;
	  		$type = BooleanType.instance;
	  	} else {
	  		throw new CompilationError("Operation '" + $relation.text + "' is not allowed for operands of type " + $type + " and " + $b.type);
	  	}
	  } )? ;

simpleExpression returns [int variable, Type type]
	: {
		String unary = null; 
	  } (unaryOperator {
	    unary = $unaryOperator.text;
	  } )? a=term {
  		$variable = $a.variable;
  		$type = $a.type;
	  	if (unary != null) {
	  		if ($type.isNumber()) {
	  			int newVariable = allocateTempVariable($type);
	  			code.add("V" + newVariable + " = " + unary + " V" + $variable);
	  			$variable = newVariable;
	  		} else {
	  			throw new CompilationError("Unary operator '" + unary + "' is not applicable to argument of type " + $type);
	  		}
	  	}
	  } (addOperator b=term {
	  	String op = $addOperator.text;
	  	if (op.equals("or")) {
	  		if ($type.equals(BooleanType.instance) && $b.type.equals(BooleanType.instance) ||
	  		    $type.equals(IntType.instance) && $b.type.equals(IntType.instance)) {
	  			int newVariable = allocateTempVariable($type);
	  			code.add("V" + newVariable + " = V" + $variable + " " + op + " V" + $b.variable);
	  			$variable = newVariable;
	  		} else {
	  			throw new CompilationError("Binary operator 'or' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  	} else {
	  		//+ or -
	  		if ($type.isNumber() && $b.type.isNumber()) {
	  			int leftVariable, rightVariable;
	  			Type maximumType;
	  			if ($type.numberLevel() < $b.type.numberLevel()) {
	  				rightVariable = $b.variable;
	  				maximumType = $b.type;
	  				leftVariable = allocateTempVariable(maximumType);
	  				code.add("V" + leftVariable + " = (" + maximumType + ") V" + $variable);
	  			} else if ($type.numberLevel() > $b.type.numberLevel()) {
	  				leftVariable = $variable;
	  				maximumType = $type;
	  				rightVariable = allocateTempVariable(maximumType);
	  				code.add("V" + rightVariable + " = (" + maximumType + ") V" + $b.variable);
	  			} else {
	  				leftVariable = $variable;
	  				rightVariable = $b.variable;
	  				maximumType = $type;
	  			}
	  			int newVariable = allocateTempVariable(maximumType);
	  			code.add("V" + newVariable + " = V" + leftVariable + " " + op + " V" + rightVariable);
	  			$variable = newVariable;
	  			$type = maximumType;
	  		} else {
	  			throw new CompilationError("Binary operator '" + op + "' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  	}
	  })* ;


term returns [int variable, Type type]
	: a=factor {
		$variable = $a.variable;
		$type = $a.type;
	  } (multOperator b=factor {
	  	String op = $multOperator.text;
	  	if (op.equals("*")) {
	  		if ($type.isNumber() && $b.type.isNumber()) {
	  			int leftVariable, rightVariable;
	  			Type maximumType;
	  			if ($type.numberLevel() < $b.type.numberLevel()) {
	  				rightVariable = $b.variable;
	  				maximumType = $b.type;
	  				leftVariable = allocateTempVariable(maximumType);
	  				code.add("V" + leftVariable + " = (" + maximumType + ") V" + $variable);
	  			} else if ($type.numberLevel() > $b.type.numberLevel()) {
	  				leftVariable = $variable;
	  				maximumType = $type;
	  				rightVariable = allocateTempVariable(maximumType);
	  				code.add("V" + rightVariable + " = (" + maximumType + ") V" + $b.variable);
	  			} else {
	  				leftVariable = $variable;
	  				rightVariable = $b.variable;
	  				maximumType = $type;
	  			}
	  			int newVariable = allocateTempVariable(maximumType);
	  			code.add("V" + newVariable + " = V" + leftVariable + " * V" + rightVariable);
	  			$variable = newVariable;
	  			$type = maximumType;
	  		} else {
	  			throw new CompilationError("Binary operator '*' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  	} else if (op.equals("/")) {
	  		if (!$type.isNumber() && !a.type.isNumber()) {
	  			throw new CompilationError("Binary operator '/' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  		int leftVariable, rightVariable;
	  		if ($type.equals(RealType.instance)) {
	  			leftVariable = $variable;
	  		} else { 
	  			leftVariable = allocateTempVariable(RealType.instance);
	  			code.add("V" + leftVariable + " = (" + RealType.instance + ") V" + $variable);
	  		}
	  		if ($b.type.equals(RealType.instance)) {
	  			rightVariable = $b.variable;
	  		} else {
	  			rightVariable = allocateTempVariable(RealType.instance);
	  			code.add("V" + rightVariable + " = (" + RealType.instance + ") V" + $b.variable);
	  		}
	  		$variable = allocateTempVariable(RealType.instance);
	  		$type = RealType.instance;
	  		code.add("V" + $variable + " = V" + leftVariable + " / V" + rightVariable);
	  	} else if (op.equals("div") || op.equals("mod")) {
	  		if ($type.equals(IntType.instance) && $b.type.equals(IntType.instance)) {
	  			int newVariable = allocateTempVariable($type);
	  			code.add("V" + newVariable + " = V" + $variable + " " + op + " V" + $b.variable);
	  			$variable = newVariable;
	  		} else {
	  			throw new CompilationError("Binary operator 'div' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  	} else if (op.equals("and")) {
	  		if ($type.equals(IntType.instance) && $b.type.equals(IntType.instance) ||
	  		    $type.equals(BooleanType.instance) && $b.type.equals(BooleanType.instance)) {
	  			int newVariable = allocateTempVariable($type);
	  			code.add("V" + newVariable + " = V" + $variable + " " + op + " V" + $b.variable);
	  			$variable = newVariable;
	  		} else {
	  			throw new CompilationError("Binary operator 'div' is not applicable to arguments of type " + $type + " and " + $b.type);
	  		}
	  	}
	  })* ;


factor returns [int variable, Type type] 
	: constant {
		$type = $constant.type;
		$variable = allocateTempVariable($type);
		code.add("V" + $variable + " = " + $constant.text);
	  }	  
	| ID {
		if (true) { throw new CompilationError("Functions are not supported yet"); }
	  } (actualParameters {
	  })+ {
	  }
	| qualifiedName {
		$type = $qualifiedName.type;
		$variable = allocateTempVariable($type);
		code.add("V" + $variable + " = " + $qualifiedName.access); 
	  }
	| '(' expression ')' { 
		$variable = $expression.variable; $type = $expression.type; 
	  } 
	| 'not' a=factor {
		if (!$a.type.equals(BooleanType.instance) && !$a.type.equals(IntType.instance)) {
			throw new CompilationError("Unary operator 'not' is not applicable to operand of type " + $a.type);
		}
		$variable = allocateTempVariable($a.type);
		$type = $a.type;
		code.add("V" + $variable + " = not " + $a.variable);
	  }
	;

unaryOperator : ('+' | '-');
	
multOperator  : ('*' | '/' | 'div' | 'mod' | 'and');

addOperator  : ('+'| '-'| 'or'); 

relation : '=' | '<>' | '<' | '>' | '<=' | '>='	;
		
functionCall
	: ID {
		if (true) { throw new CompilationError("Functions are not supported yet"); }
	  }(actualParameters)? ;

subprogDeclList : (procedureDecl ';' | functionDecl ';')* ;
 
procedureDecl : procedureHeading ';' block ;

functionDecl : functionHeading ':' type ';' block ;

procedureHeading : 'procedure' ID (formalParameters)? ;

functionHeading : 'function' ID (formalParameters)? ;

formalParameters : '(' oneFormalParam (';' oneFormalParam)* ')' ;

oneFormalParam : 'var'? idList ':' type ;

qualifiedList returns [List<String> access, List<Type> types]
	: {
		$access = new ArrayList<String>();
		$types = new ArrayList<Type>();
	  } a=qualifiedName {
	  	$access.add($a.access);
	  	$types.add($a.type);
	  } (',' b=qualifiedName {
	  	$access.add($b.access);
	  	$types.add($b.type);
	  })*;

qualifiedName returns [String access, Type type] 
	: ID {
		int id = resolveID($ID.text);
		$type = resolveType($ID.text);
		$access = "V" + id;
	  } (qualifiedStuff {
	  	List<Integer> ids = $qualifiedStuff.ids;
	  	List<Type> types = $qualifiedStuff.types;
	  	int indexVariable = -1;
	  	for (int i = 0; i < ids.size(); ++i) {
	  		if ($type instanceof ArrayType) {
	  			ArrayType arr = (ArrayType) ($type);
	  			if (arr.indexType.equals(types.get(i))) {
	  				int tmpVariable = allocateTempVariable(IntType.instance);
	  				if (types.get(i).equals(IntType.instance)) {
	  					if (arr.lowerBound == 0) {
	  						code.add("V" + tmpVariable + " = V" + ids.get(i));
	  					} else {
	  						code.add("V" + tmpVariable + " = V" + ids.get(i) + " - " + arr.lowerBound);
	  					}
	  				} else {
	  					code.add("V" + tmpVariable + " = (" + IntType.instance + ") V" + ids.get(i));
	  					if (arr.lowerBound != 0) {
	  						code.add("V" + tmpVariable + " = V" + tmpVariable + " - " + arr.lowerBound);
	  					}
	  				}
	  				if (indexVariable != -1) {
	  					code.add("V" + indexVariable + " = V" + indexVariable + " * " + (arr.upperBound - arr.lowerBound + 1));
	  					code.add("V" + indexVariable + " = V" + indexVariable + " + V" + tmpVariable);
					} else {
						indexVariable = tmpVariable;
					}
					$type = arr.baseType;
	  			} else {
		  			throw new CompilationError("Index type mismatch at depth " + (i + 1) + 
		  									   ": expected " + arr.indexType + ", found " + types.get(i));
	  			}
	  		} else {
	  			throw new CompilationError("Expected array of depth " + ids.size() + ", found " + resolveType($ID.text));
	  		}
	  	}
	  	if ($type instanceof ArrayType) {
			throw new CompilationError("Too few indices (" + ids.size() + ") for variable of type " + resolveType($ID.text));
	  	}
	  	//Change here if you want the pointer notation to be generated.
	  	$access = "V" + id + "[V" + indexVariable + "]";
	  })?
	;

qualifiedStuff returns [List<Integer> ids, List<Type> types]	
	: {
		$ids = new ArrayList<Integer>();
		$types = new ArrayList<Type>();
	  } ('[' expList { 
	  	$ids.addAll($expList.ids); 
	  	$types.addAll($expList.types);
	  } ']')+;

ID  :	('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
    ;

INT :	('0'..'9')+
    ;

FLOAT
    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
    |   '.' ('0'..'9')+ EXPONENT?
    |   ('0'..'9')+ EXPONENT
    ;

WS  :   ( ' '
        | '\t'
        | '\r'
        | '\n'
        ) {$channel=HIDDEN;}
    ;

CHAR:  '\'' ( ESC_SEQ | ~('\''|'\\') ) '\''
    ;

fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

fragment
HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;

fragment
ESC_SEQ
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    |   UNICODE_ESC
    |   OCTAL_ESC
    ;

fragment
OCTAL_ESC
    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7')
    ;

fragment
UNICODE_ESC
    :   '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
    ;
